package kpython.backend.semantic;

import kpython.ASTAndExpr;
import kpython.ASTArgumentList;
import kpython.ASTAssignmentStmt;
import kpython.ASTAtom;
import kpython.ASTBreakStmt;
import kpython.ASTCallFunction;
import kpython.ASTCallList;
import kpython.ASTCompOper;
import kpython.ASTComparison;
import kpython.ASTCompoundStmt;
import kpython.ASTCondExpr;
import kpython.ASTContinueStmt;
import kpython.ASTDefFunction;
import kpython.ASTElif;
import kpython.ASTElse;
import kpython.ASTExpr;
import kpython.ASTFactorOper;
import kpython.ASTFor;
import kpython.ASTForStmt;
import kpython.ASTIdentifier;
import kpython.ASTIf;
import kpython.ASTIfStmt;
import kpython.ASTList;
import kpython.ASTListDisplay;
import kpython.ASTLiteral;
import kpython.ASTLiteralFloat;
import kpython.ASTLiteralInteger;
import kpython.ASTLiteralString;
import kpython.ASTNotExpr;
import kpython.ASTOrExpr;
import kpython.ASTParameterList;
import kpython.ASTPrimary;
import kpython.ASTPrintStmt;
import kpython.ASTReturnStmt;
import kpython.ASTSimpleStmt;
import kpython.ASTStart;
import kpython.ASTStmt;
import kpython.ASTSuite;
import kpython.ASTTermOper;
import kpython.ASTWhile;
import kpython.ASTWhileStmt;
import kpython.KPythonNode;
import kpython.ParserVisitor;
import kpython.SimpleNode;

/**
 * Recorrendo ao Design Pattern Visitor, esta classe visita recursivamente
 * todos os nos da AST, fazendo as verificacoes correctas para 
 * a criacao da tabela de simbolos e verificacao de erros semanticos
 * 
 * @author Joel Cordeiro
 *
 */
public class SymbolTableBuilder implements ParserVisitor {

    private SymbolTable symTable;
    private int nErrors;

    public SymbolTableBuilder() {
        this.symTable = new SymbolTable();
        this.nErrors = 0;
    }


    @Override
    public Object visit(SimpleNode node, Object data) {
        return node.childrenAccept(this, data);
    }

    @Override
    public Object visit(ASTListDisplay node, Object data) {
        return node.jjtGetChild(0).jjtAccept(this, data);
    }

    //devolve o nó filho (LiteralString, Integer ou Float)
    @Override
    public Object visit(ASTLiteral node, Object data) {
        Object toReturn = node.jjtGetChild(0).jjtAccept(this, data);
        return toReturn;
    }

    @Override
    public Object visit(ASTIdentifier node, Object data) {
        Record sym = symTable.lookup(Symbol.getSymbol(node.getSymbol()));
        if (sym == null) {
            System.out.println("ERROR [semantic] : Undeclared variable " + node.getSymbol()+
            		" Line: " + node.getLine() +
                    " Column: " + node.getColumn());
            nErrors++;
            return null;
        }
        return sym.getType();
    }

    @Override
    public Type visit(ASTLiteralFloat node, Object data) {
        return Type.floatType;
    }

    @Override
    public Type visit(ASTLiteralInteger node, Object data) {
        return Type.intType;
    }

    @Override
    public Type visit(ASTLiteralString node, Object data) {
        return Type.stringType;
    }

    @Override
    public Object visit(ASTAtom node, Object data) {
        return node.jjtGetChild(0).jjtAccept(this, data);
    }
    //Call
    @Override
    public Object visit(ASTArgumentList node, Object data) {
        return node.jjtGetChild(0).jjtAccept(this, data);
    }

    @Override
    public Object visit(ASTCallFunction node, Object data) {
        ASTIdentifier aid = (ASTIdentifier) node.jjtGetChild(0);
        Record r = symTable.lookup(Symbol.getSymbol(aid.getSymbol()));
        //Verificacao de que o registo existe
        if (r == null) {
            System.out.println("ERROR [semantic] : Undeclared function - " + aid.getSymbol() +
                    		" Line: " + aid.getLine() +
                            " Column: " + aid.getColumn());
            nErrors++;
            return Type.unknownType;
        }
        //Verificacao de que se trata de uma funcao
        if (!(r instanceof FunctionRecord)) {
            System.out.println("ERROR [semantic] : Not a function called - " + aid.getSymbol());
            nErrors++;
            return Type.unknownType;
        }
        
        ASTDefFunction functionDef = (ASTDefFunction) symTable.getFunctionDef(aid.getSymbol());
        if(functionDef == null){
        	return Type.unknownType;
        }
        Scope s = symTable.getScope(aid.getSymbol());
        
        //Verificação do numero de parametros
        int nParams = 0;
        int childToVisit = 1;
        ASTArgumentList args = null;
        if (node.jjtGetNumChildren() > 1) {
            args = (ASTArgumentList) node.jjtGetChild(1);
            nParams = args.jjtGetNumChildren();
        }
    
        if (((FunctionRecord) r).getNparams() != nParams) {
            System.out.println("ERROR [semantic] : Invalid number of parameters, given " + nParams + " needed " + ((FunctionRecord) r).getNparams());
            nErrors++;
            return Type.unknownType;
        }
            // this is protected because if no children exist then the loop doesn't occur
        if (nParams > 0){
        	childToVisit = 2;
	        for (int i = 0; i < args.jjtGetNumChildren(); i++) {
	            ASTIdentifier arg = (ASTIdentifier) functionDef.jjtGetChild(1).jjtGetChild(i);
	            s.put(Symbol.getSymbol(arg.getSymbol()), 
	                    new VarRecord(node.getLine(), 
	                        node.getColumn(), 
	                        Symbol.getSymbol(arg.getSymbol()), 
	                        (Type) args.jjtGetChild(i).jjtAccept(this, data)
	                        ));
	        }
        }
        //visit the function child nodes
        Scope currentScope = symTable.getCurrentScope();
        symTable.goToScope(s);
        functionDef.jjtGetChild(childToVisit).jjtAccept(this, data);
        symTable.goToScope(currentScope);
        
        
        return ((Record) symTable.lookup(Symbol.getSymbol( ((ASTIdentifier) functionDef.jjtGetChild(0)).getSymbol()))).getType(); 
    }

    @Override
    public Object visit(ASTPrimary node, Object data) {
        return node.jjtGetChild(0).jjtAccept(this, data);
    }

    @Override
    public Object visit(ASTAssignmentStmt node, Object data) {

        ASTAtom a = (ASTAtom) node.jjtGetChild(0);
        ASTIdentifier aid = (ASTIdentifier) a.jjtGetChild(0);
        Object type = node.jjtGetChild(1).jjtAccept(this, data);
        if (type == null) {
            return new Boolean(false);
        }
        symTable.put(Symbol.getSymbol(aid.getSymbol()),
                new VarRecord(node.getLine(), node.getColumn(), Symbol.getSymbol(aid.getSymbol()), (Type) type));

        return new Boolean(true);
    }

    @Override
    public Object visit(ASTPrintStmt node, Object data) {
        return node.jjtGetChild(0).jjtAccept(this, data);
    }

    // + -
    @Override
    public Object visit(ASTTermOper node, Object data) {
        if (node.jjtGetNumChildren() == 1) {
            return node.jjtGetChild(0).jjtAccept(this, data);
        }
        
        Type a = (Type) node.jjtGetChild(0).jjtAccept(this, data);
        Type b = (Type) node.jjtGetChild(1).jjtAccept(this, data);
        
        if(a == null )
            return null;

        if (a == Type.intType && b == Type.floatType) {
            return Type.floatType;
        }
        if (b == Type.intType && a == Type.floatType) {
            return Type.floatType;
        }
        if (a.equals(b)) {
            return a;
        }
        System.out.println("ERROR [semantic] : Type mismatch. " +
                " Line: " + node.getLine() +
                " Column: " + node.getColumn());
        nErrors++;

        return null;
    }

    // * / %
    @Override
    public Object visit(ASTFactorOper node, Object data) {
        if (node.jjtGetNumChildren() == 1) {
            return node.jjtGetChild(0).jjtAccept(this, data);
        }
        Type a = (Type) node.jjtGetChild(0).jjtAccept(this, data);
        Type b = (Type) node.jjtGetChild(1).jjtAccept(this, data);
        if(a == null )
            return null;
        if (a == Type.intType && b == Type.floatType) {
            return Type.floatType;
        }
        if (b == Type.intType && a == Type.floatType) {
            return Type.floatType;
        }
        if (a.equals(b)) {
            return a;
        }
        System.out.println("ERROR [semantic] : Type mismatch. " +
                " Line: " + node.getLine() +
                " Column: " + node.getColumn());
        nErrors++;
        return null;
    }

    // == != <= >=
    @Override
    public Object visit(ASTComparison node, Object data) {
        if (node.jjtGetNumChildren() == 1) {
            return node.jjtGetChild(0).jjtAccept(this, data);
        }

        Type a = (Type) node.jjtGetChild(0).jjtAccept(this, data);
        Type b = (Type) node.jjtGetChild(2).jjtAccept(this, data);

        if(a == null )
            return null;
        if (a == Type.intType && b == Type.floatType) {
            return Type.floatType;
        }
        if (b == Type.intType && a == Type.floatType) {
            return Type.floatType;
        }
        if (a.equals(b)) {
            return a;
        }
        
        System.out.println("ERROR [semantic] : Type mismatch. " +
                " Line: " + ((KPythonNode) node.jjtGetChild(0)).getLine() +
                " Column: " + ((KPythonNode) node.jjtGetChild(0)).getColumn());
        nErrors++;
        return null;
    }

    @Override
    public Object visit(ASTNotExpr node, Object data) {
        return node.jjtGetChild(0).jjtAccept(this, data);
    }

    @Override
    public Object visit(ASTAndExpr node, Object data) {
        if (node.jjtGetNumChildren() == 1) {
            return node.jjtGetChild(0).jjtAccept(this, data);
        }
        Type a = (Type) node.jjtGetChild(0).jjtAccept(this, data);
        Type b = (Type) node.jjtGetChild(1).jjtAccept(this, data);
        if(a == null )
            return null;
        if (a == Type.intType && b == Type.floatType) {
            return Type.floatType;
        }
        if (b == Type.intType && a == Type.floatType) {
            return Type.floatType;
        }
        if (a.equals(b)) {
            return a;
        }
        System.out.println("ERROR [semantic] : Type mismatch. " +
                " Line: " + node.getLine() +
                " Column: " + node.getColumn());
        nErrors++;

        return null;
    }

    @Override
    public Object visit(ASTOrExpr node, Object data) {
        if (node.jjtGetNumChildren() == 1) {
            return node.jjtGetChild(0).jjtAccept(this, data);
        }
        Type a = (Type) node.jjtGetChild(0).jjtAccept(this, data);
        Type b = (Type) node.jjtGetChild(1).jjtAccept(this, data);

        if (a == Type.intType && b == Type.floatType) {
            return Type.floatType;
        }
        if (b == Type.intType && a == Type.floatType) {
            return Type.floatType;
        }
        if (a.equals(b)) {
            return a;
        }
        System.out.println("ERROR [semantic] : Type mismatch. " +
                " Line: " + node.getLine() +
                " Column: " + node.getColumn());
        nErrors++;

        return null;
    }

    @Override
    public Object visit(ASTCondExpr node, Object data) {
        return node.jjtGetChild(0).jjtAccept(this, data);
    }

    @Override
    public Object visit(ASTExpr node, Object data) {
        return node.jjtGetChild(0).jjtAccept(this, data);
    }

    @Override
    public Object visit(ASTSimpleStmt node, Object data) {
        return node.jjtGetChild(0).jjtAccept(this, data);
    }

    @Override
    public Object visit(ASTContinueStmt node, Object data) {
        return node.jjtGetChild(0).jjtAccept(this, data);
    }

    @Override
    public Object visit(ASTBreakStmt node, Object data) {
        return node.jjtGetChild(0).jjtAccept(this, data);
    }

    @Override
    public Object visit(ASTReturnStmt node, Object data) {
        KPythonNode suite = (KPythonNode) node.jjtGetParent().jjtGetParent().jjtGetParent();
        KPythonNode def = (KPythonNode) suite.jjtGetParent();
        ASTIdentifier aid =  (ASTIdentifier)def.jjtGetChild(0);
        
        Record function = symTable.lookup(Symbol.getSymbol(aid.getSymbol()));
        function.setType((Type) node.jjtGetChild(0).jjtAccept(this, data));
        return new Boolean(true);
    }
    
    //definition
    @Override
    public Object visit(ASTParameterList node, Object data) {
        String id;
        for (int i = 0; i < node.jjtGetNumChildren(); i++) {
            ASTIdentifier child = (ASTIdentifier) node.jjtGetChild(i);
            id = child.getSymbol();
            symTable.put(Symbol.getSymbol(id),
                    new VarRecord(child.getLine(), child.getColumn(), Symbol.getSymbol(id), Type.unknownType));
        }
        return node.childrenAccept(this, data);
    }

    @Override
    public Object visit(ASTElif node, Object data) {
        symTable.enterScope("elif");
        node.jjtGetChild(0).jjtAccept(this, data);
        symTable.exitScope();
        return new Boolean(true);
    }

    @Override
    public Object visit(ASTElse node, Object data) {
        symTable.enterScope("else");
        node.jjtGetChild(0).jjtAccept(this, data);
        symTable.exitScope();
        return new Boolean(true);
    }

    @Override
    public Object visit(ASTIf node, Object data) {
        symTable.enterScope("if");
        node.jjtGetChild(0).jjtAccept(this, data);
        symTable.exitScope();
        return new Boolean(true);
    }

    @Override
    public Object visit(ASTIfStmt node, Object data) {
        return node.jjtGetChild(0).jjtAccept(this, data);
    }

    @Override
    public Object visit(ASTFor node, Object data) {

        return node.jjtGetChild(0).jjtAccept(this, data);
    }

    @Override
    public Object visit(ASTForStmt node, Object data) {

        return node.jjtGetChild(0).jjtAccept(this, data);
    }

    @Override
    public Object visit(ASTWhile node, Object data) {

        return node.jjtGetChild(0).jjtAccept(this, data);
    }

    @Override
    public Object visit(ASTWhileStmt node, Object data) {

        return node.jjtGetChild(0).jjtAccept(this, data);
    }

    @Override
    public Object visit(ASTDefFunction node, Object data) {

        //Verifica se o identificador já existe
        String name = ((ASTIdentifier) node.jjtGetChild(0)).getSymbol();
        if (symTable.currentLookup(Symbol.getSymbol(name)) != null) {
            System.out.println("ERROR [semantic] : Identifier already in use - " + name);
            nErrors++;
        } else {
            Object args = node.jjtGetChild(1);
            Symbol sym = Symbol.getSymbol(name);
            //Se houver parametros
            if (args instanceof ASTParameterList) {
                //passa o num de parametros
                symTable.put(sym, new FunctionRecord(node.jjtGetChild(1).jjtGetNumChildren(), node.getLine(), node.getColumn(), sym));
            this.symTable.enterScope(name);
            this.symTable.putFunctionDef(name, node);
            this.symTable.exitScope();
            } //Senao
            else {
                //Mete a 0
                symTable.put(sym, new FunctionRecord(0, node.getLine(), node.getColumn(), sym));
                this.symTable.enterScope(name);
                this.symTable.putFunctionDef(name, node);
                node.childrenAccept(this, data);
                this.symTable.exitScope();
            }
        }
        return new Boolean(true);
    }

    @Override
    //if, for, while
    public Object visit(ASTCompoundStmt node, Object data) {
        return node.jjtGetChild(0).jjtAccept(this, data);
    }

    @Override
    //def function, simple stmt, compound stmt
    public Object visit(ASTStmt node, Object data) {
        return node.jjtGetChild(0).jjtAccept(this, data);
    }

    @Override
    public Object visit(ASTSuite node, Object data) {
        return node.childrenAccept(this, data);
    }
    

    @Override
    public Object visit(ASTStart node, Object data) {
        symTable.enterScope("main");
        node.childrenAccept(this, data);
        symTable.exitScope();
        return this.symTable;
    }

    public Object visit(ASTCompOper node, Object data) {
        return node.childrenAccept(this, data);
    }

    public Object visit(ASTList node, Object data) {
        Type t = Type.listUnknownType;
        switch(node.jjtGetNumChildren()) {
            case 0 : return t;
            case 1 : return Type.getListType((Type) node.jjtGetChild(0).jjtAccept(this, data));
            default : {
                t = (Type) node.jjtGetChild(0).jjtAccept(this, data);
                for (int i=1; i< node.jjtGetNumChildren(); i++) {
                    if ((Type) node.jjtGetChild(i).jjtAccept(this, data) != t) {
                        System.out.println("ERROR [semantic] : List type mismatch. " +
                        " Line: " + node.getLine() +
                        " Column: " + node.getColumn());
                        nErrors++;
                        return Type.listUnknownType;
                    }
                }
                return Type.getListType(t);
            }         
        }        
    }

    public Object visit(ASTCallList node, Object data) {
        //Identifier() t = <LBRACKET> ( LiteralInteger() | <COLON> ) <RBRACKET>
        throw new UnsupportedOperationException("Not supported yet.");
    }

    public boolean hasErrors() {
        return this.nErrors > 0;
    }

    public int getNumErrors() {
        return this.nErrors;
    }

}
